
# Defines a smart Sprite class and an overloaded Target sprite subclass
# This two classes are the foundation of the graphical enviroment.
# Sprite and Target are designed to be used as base classes

from __future__ import division

# Required pygame imports
import pygame, weakref, math
from pygame.locals import *
from pygame import Surface

# Relative pyxel modules
from library import Resource
from utils import rectify


# Custom modified sprite class
# This class defines behavior that is common to ALL game sprites
class Sprite(pygame.sprite.Sprite):
    
    def __init__(self, image = None, rect = (0,0), vgroup = None, show = True):
        pygame.sprite.Sprite.__init__(self)
        
        # Correct the rect size
        self.__image = None
        self.rect = rectify(rect)
        
        # If image is a string, load it from the Resource system
        if isinstance(image, basestring): self.image = Resource(image)
        else: self.image = image
        
        # Sprite visibility attributes and defaults
        self.__vgroup = vgroup
        self.__hsrect = None
        self.visible = show
        
        # Setup the sprites auto-move functionality
        self.route = []
        
        
    # Set a rally point for the sprite
    def rally(self, point, delay, cback = None):
        point = list(point)
        if point[0] is None: point[0] = self.rect.centerx
        if point[1] is None: point[1] = self.rect.centery        
        self.route.append([point, int(delay), cback])
        self.notify("rect", self.rect)
        
    
    # Perform automatic sprite manteinance and repositioning
    def update(self, delta):
        if len(self.route) >= 1:
            px, py = self.route[0][0]
            rx, ry = self.rect.center
            lapse = self.route[0][1]
            
            # If the rally point is really close, just jump to it
            if lapse <= delta:
                self.rect.center, zero, cback = self.route.pop(0)
                if callable(cback): cback()
            
            # If not advace the image acccording to elapsed time
            else:
                self.rect.move_ip(*[i * (delta / lapse) for i in (px-rx, py-ry)])
                self.route[0][1] -= delta

                
    # Remove all rally points
    def stop(self): del self.route[:]
    
    # Reports if the sprite is currently in rally status
    def moving(self): return bool(self.route)
        
        
    # Make sure that the target's rect size is updated automatically
    # whenever the surface size changes
    def get_image(self): return self.__image
    
    def set_image(self, image):
        if image is not None: 
            assert isinstance(image, Surface), "Invalid Surface"
            self.__image = image
            self.rect.size = self.__image.get_size()
        else: self.rect.size = (0,0)
            
    def del_image(self): self.image = None
    
    image = property(get_image, set_image, del_image)

                
    # Control the sprites visibility system:
    
    def hide(self): 
        if self.__vgroup is not None: 
            self.__vgroup.remove(self)
        if self.__hsrect: 
            self.rect = self.__hsrect
            self.__hsrect = None
        
    def show(self): 
        if self.__vgroup is not None and self.image is not None: 
            self.__vgroup.add(self)
        
    def get_visible(self):
        if self.__vgroup is None: return None         
        return self.__vgroup in self.groups()        
    
    def set_visible(self, val):
        if val: self.show()
        else: self.hide()
    
    visible = property(get_visible, set_visible, None)
        
    # Sprite Transformations:
        
    # Rotate and zoom the sprite in one step
    def rotozoom(self, angle=0, scale=1):
        x, y = self.rect.topleft
        self.image = transform.rotozoom(self.image, angle, scale)
        dx, dy = self.image.get_size()
        self.rect.topleft = (x - dx/2, y - dy/2)
        
    # Make a whole surface translucent
    def translucify(self, mask):
        if mask is None: mask = 255 # If no mask given, remove translucency
        assert mask > 0 and mask <= 255, "Mask value must be between 1 and 255."
        img = self.image.convert_alpha()
        img.lock()
        for x in range(img.get_width()):
            for y in range(img.get_height()):
                c = img.get_at((x,y))
                if c[3] != 0:
                    img.set_at((x,y), (c[0],c[1],c[2], mask))
        img.unlock()
        self.image = img
        
    # Align this sprite to a given rect
    def align(self, pos, align='top,bottom', margin=5, bind=True, rally=None):
        if bind: 
            screen = pygame.display.get_surface().get_rect()
        if ':' in align: 
            anchor, align = align.split(':', 1)
        else: anchor = 'center'
        rect = Rect(self.rect)
        
        for dir in align.split(','):
            exec "rect.%s = pos.%s" % (anchor, anchor)
            if dir in ('left', 'right'): 
                dist = (rect.width / 2) + (pos.width / 2) + margin
                if dir == 'right': rect.left += dist
                else: rect.left -= dist
            elif dir in ('top', 'bottom'):
                dist = (rect.height / 2) + (pos.height / 2) + margin
                if dir == 'bottom': rect.top += dist
                else: rect.top -= dist
            if bind and screen.contains(rect): break
        else:
            if bind: self.rect.clamp_ip(rect)
            
        if rally is not None: self.rally(rect.center, rally)
        else: self.rect = rect
        
        
    # This method moves the sprite's center to a point, then
    # uses the rally method to slide it back to place. 
    def nudge(self, point, back_lapse, init_lapse=0):
        self.rally(point, init_lapse)
        self.rally(self.rect.center, back_lapse)
        
    # Sends a sprite to a point, then hides it and resets the
    # original rect for the sprite
    def sweep(self, point, lapse):
        self.__hsrect = Rect(self.rect)
        self.rally(point, lapse, self.hide)
                



# Defines a special sprite that can be interacted with the pointer
# Also, all targets can be located with a string identifier
class Target(Sprite):
    
    # A mapping of all Target instances that have an id
    index =  weakref.WeakValueDictionary()
    
    # A meta-list used for z-ordering, but not for rendering
    __zlist = pygame.sprite.OrderedUpdates()
    
    # Interface system sprites:
    # There can only be zero or one focused and grabbed sprites at once
    focus = pygame.sprite.GroupSingle()
    grab = pygame.sprite.GroupSingle()
    
    def __init__(self, id = None, offset=None, drag=False, **kwargs):

        # List of callback watching this target
        self.watchers = dict()
        
        # Initialize base Sprite properties
        self.oldrect = Rect(0,0,0,0)
        Sprite.__init__(self, **kwargs)
        
        # A target must either provide a unique id or have no id at all
        # If the target has no id, it will not be possible to locate it in a search
        if id is not None:
            Target.index[id] = self
        self.id = id

        # By default, you can make the target invoke another sprite to be
        # shown during a mouseover. The use of a Tooltip instance is even better
        self.mouseover = None

        # Enable dragging this sprite
        self.draggable = drag
        self.drag_start = None

        # Parent container relationship
        self.container = None
        
    # Allows another object to receive updates on some of this objects properties
    def watch(self, attribute, callback):
        assert isinstance(attribute, basestring), "Invalid attribute " + attribute
        assert callable(callback), "Invalid callback " + callback
        if attribute in self.watchers:
            self.watchers[attribute].append(callback)
        else:
            self.watchers[attribute] = [callback]

    # Notifies watchers of an attribute update
    def notify(self, attribute, value = None):
        for callback in self.watchers.get(attribute, []):
            callback(attribute, value, self)
        return value
            
        
    # Meta-update method. It calls a user defined update method
    # but only when the values of watched for objects have changed
    # It also notifies other objects of changes in its values
    def update(self, delta):
        super(Target, self).update(delta)

        # Touch the rect attribute if size or position has changed
        if self.oldrect != self.rect:
            self.oldrect = self.notify('rect', self.rect)
            

            
    # Returns the Target ID
    def __str__(self): return self.__dict__.get(id, 'Unknown ' + str(type(self)))
        
    # Removes this target from all groups and the master index
    def remove(self):
        if self.mouseover: self.mouseover.remove()
        if self.id: del Target.index[self.id]
        Target.__zlist.remove(self); self.kill()
        
        
    # Target overloads the show/hide methods to implement z-ordering
    def hide(self):
        if self in Target.__zlist: Target.__zlist.remove(self)
        super(Target, self).hide()
        self.notify('visibile', False)
        
        
    def show(self):
        super(Target, self).show()
        if self.visible: Target.__zlist.add(self)
        self.notify('visibile', True)
        
    # Find the topmost Iteractible under the given coordinates
    # Return None if no Target is located there
    @staticmethod
    def at(pt):
        rect = rectify(pt)
        for s in filter(lambda x: x.rect.contains(rect), iter(Target.__zlist)):
            offset = (rect[0] - s.rect[0], rect[1] - s.rect[1])
            pixel = s.image.get_at(offset)
            if pixel != s.image.get_colorkey() and pixel[3] and not Target.grab.has(s):
                return s
                
    # Verify and return the named target if it exists
    @staticmethod
    def search(target):
        if isinstance(target, basestring): return Target.index.get(target)
        elif isinstance(target, Target): return target
        else: raise ValueError('Target must be an instance or string id')
        
                    
    # Hooks: This methods do not do anything on their own,
    # but the game engine calls them when the player interacts with the Target
    
    def click(self, pos): self.notify('click', 'left')
    def right_click(self): self.notify('click', 'right')
    def mid_click(self): self.notify('click', 'middle')
    def double_click(self): self.notify('click', 'double')
    
    def scroll_up(self): self.notify('scroll', 'up')
    def scroll_down(self): self.notify('scroll', 'down')
    
    # There is a default behavior for hover/leave: tooltips
    # If you want to override, but keep the tooltip functionality, make
    # sure that your method calls this one via super()
    def hover(self): 
        if self.mouseover is not None: 
            self.mouseover.show()
            self.notify("rect", self.rect)
        self.notify('hover', True)
        
    def leave(self): 
        if self.mouseover is not None: self.mouseover.hide()
        self.notify('hover', False)
    
    def drag(self, pos):
        if self.drag_start is None:
            self.drag_start = self.rect.center
            self.leave()
        self.rect.center = pos
        self.notify("rect", self.rect)
        
    def drop(self, obj):
        if obj is None:
            if self.drag_start:
                self.rally(self.drag_start, 250)
        else:
            if obj.receive(self):
                pass
            else:
                if self.drag_start:
                    self.rally(self.drag_start, 250)
        self.drag_start = None

    # Called when another target is dropped on this one
    def receive(self, target):
        return False
        











        

